/*
	usteve

	camera

  	FILE NAME:
  
  		camera.h
  
  	DESCRIPTION:
		
		A Camera class that works like a typical FPS camera

  	MODIFICATION HISTORY:
  
  		20-DEC-2008 Phillip Napieralski - Wrote the code 
*/

#pragma once

#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <gl/glut.h>

#include "support3d.h"

const Point3 ORIGIN = Point3(0,0,0);
const Vector3 DEFAULT_LOOK_AT = Vector3( 0, 1.0f, 0.0f );
const Vector3 DEFAULT_UP_VECTOR = Vector3( 0, 0.0f, 1.0f );
const double DEFAULT_V_ANG = 30.0;
const double DEFAULT_ASPECT_RATIO = 4.0/3.0;
const double DEFAULT_NEAR_DISTANCE = 0.01;
const double DEFAULT_FAR_DISTANCE = 1000.0;
const double MAX_VIEWING_ANGLE = 1.0f; /* Max pitch viewing angle (in radians) */

class Camera
{
public:
	Camera()
	{ 
		setDefault(); 
	}

	void set( Point3 pos, Vector3 look, Vector3 up );
	void setShape( double vAng, double asp, double nearD, double farD );
	void setDefault();

	void applyShape();

	Point3 getPosition() { return position; }
	Point3 getLookAt() { return lookAt; }
	Vector3 getLookDirection()
	{
		return Vector3( lookAt.x - position.x, lookAt.y - position.y, lookAt.z - position.z );
	}
	Vector3 getUpVector() { return upVector; }
	Vector3 getStrafe() { return strafe; }

	/* Rotates the camera's view based on the angle & axis provided */
	void rotateAroundVector( float angle, float xx, float yy, float zz );
	void rotateAroundVector( float angle, Vector3 vec );

	void yaw( float angle );
	void lookUp( float angle );
	void roll( float angle );


	/* Moves camera right. Pass a negative to move left */
	void moveRight( float speed );

	/* Move camera forward. Pass a negative to move backward */
	void moveForward( float speed );

	void look();

	void jump();

private:
	Point3 position;
	Point3 lookAt; /* What point are we looking at? */
	Vector3 upVector;
	Vector3 strafe;

	double viewAngle, aspect, nearDist, farDist;

	void _calculateStrafe();
};

void Camera::_calculateStrafe()
{
	Vector3 lookVec;
	Vector3 newStrafe(upVector);

	lookVec.setDiff( lookAt, position );

	/* lookVec (x) upVector will give us the strafe vector */
	newStrafe = lookVec.cross( newStrafe );
	newStrafe.normalize();

	strafe.set(newStrafe);
}

void Camera::set( Point3 pos, Vector3 look, Vector3 up )
{
	position = Point3( pos.x, pos.y, pos.z );
	lookAt = Point3( look.x, look.y, look.z );
	upVector = Vector3( up.x, up.y, up.z );

	_calculateStrafe();
}

void Camera::setShape(double vAng, double asp, double nearD, double farD)
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(vAng, asp, nearD, farD);

	viewAngle = vAng;
	aspect = asp;
	nearDist = nearD;
	farDist = farD;
}

void Camera::applyShape()
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective( viewAngle, aspect, nearDist, farDist );
}

void Camera::setDefault()
{
	/* Start at origin, looking up and out, and z-axis is up */
	position = Point3(ORIGIN.x, ORIGIN.y, ORIGIN.z);
	lookAt = Point3(DEFAULT_LOOK_AT.x, DEFAULT_LOOK_AT.y, DEFAULT_LOOK_AT.z );
	upVector = Vector3(DEFAULT_UP_VECTOR);
	setShape( DEFAULT_V_ANG, DEFAULT_ASPECT_RATIO,
			  DEFAULT_NEAR_DISTANCE, DEFAULT_FAR_DISTANCE );

	_calculateStrafe();
}

void Camera::rotateAroundVector( float angle, float x, float y, float z )
{
	//return ;
	Vector3 currLook;
	Vector3 perp;
	Vector3 newLook;

	float cosTheta = (float)cos(angle);
	float sinTheta = (float)sin(angle);
	float nDotX = 0.0f;

	/* Use Rodrigues' Rotation Formula to compute the new lookAt point 
	 * http://mathworld.wolfram.com/RodriguesRotationFormula.html
	 */

	currLook.setDiff( lookAt, position );

	newLook.x  = (cosTheta + (1 - cosTheta) * x * x) * currLook.x;
	newLook.x += ((1 - cosTheta) * x * y - z * sinTheta) * currLook.y;
	newLook.x += ((1 - cosTheta) *x * z + y * sinTheta) * currLook.z;



	newLook.y  = ((1 - cosTheta) * x * y + z * sinTheta) * currLook.x;
	newLook.y += (cosTheta + (1 - cosTheta) * y * y) * currLook.y;
	newLook.y += ((1 - cosTheta) * y * z - x * sinTheta) * currLook.z;
	lookAt = Point3(position.x + newLook.x, position.y + newLook.y, position.z + newLook.z ); 


	newLook.z  = ((1 - cosTheta) * x * z - y * sinTheta) * currLook.x;
	newLook.z += ((1 - cosTheta) * y * z + x * sinTheta) * currLook.y;
	newLook.z += (cosTheta + (1 - cosTheta) * z * z) * currLook.z;

	lookAt = Point3(position.x + newLook.x, position.y + newLook.y, position.z + newLook.z ); 

}

void Camera::rotateAroundVector( float angle, Vector3 vec )
{
	rotateAroundVector( angle, vec.x, vec.y, vec.z );
}

void Camera::moveRight( float speed )
{
	position.x += strafe.x * speed;
	position.y += strafe.y * speed;
	position.z += strafe.z * speed;

	lookAt.x += strafe.x * speed;
	lookAt.y += strafe.y * speed;
	lookAt.z += strafe.z * speed;
}

void Camera::moveForward( float speed )
{
	/* get the direction we are looking */
	Vector3 lookVec;
	lookVec.setDiff( lookAt, position );

	lookVec.normalize();

	position.x += lookVec.x * speed;
	position.y += lookVec.y * speed; 
	//position.z += lookVec.z * speed;
	
	lookAt.x += lookVec.x * speed;
	lookAt.y += lookVec.y * speed;
}

void Camera::look()
{
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	gluLookAt( position.x, position.y, position.z,
			   lookAt.x, lookAt.y, lookAt.z,
			   upVector.x, upVector.y, upVector.z );
}

void Camera::yaw( float angle )
{
	rotateAroundVector( -angle, upVector );
}

void Camera::lookUp( float angle )
{
	angle = -angle;
	static float currentRot = 0.0f;

	_calculateStrafe();

	currentRot -= angle;

	if( currentRot > MAX_VIEWING_ANGLE )
		currentRot = MAX_VIEWING_ANGLE;
	else if( currentRot < -MAX_VIEWING_ANGLE )
		currentRot = -MAX_VIEWING_ANGLE;
	else
		rotateAroundVector( angle, strafe );
}

void Camera::roll( float angle )
{
	cerr << "Roll not implemented yet!" << endl;
}
